Skip to content

Name the component, not its element, in RenderError — and report mount, host-root and ErrorBoundary-caught render errors - #1323

Merged
Alexandre Zollinger Chohfi (azchohfi) merged 19 commits into
mainfrom
azchohfi-render-error-component-name
Oct 7, 2026
Merged

Alexandre Zollinger Chohfi (azchohfi) merged 19 commits into
mainfrom
azchohfi-render-error-component-name

Conversation

@azchohfi

@azchohfi Alexandre Zollinger Chohfi (azchohfi) commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

ReactorEventSource.RenderError (EventId 9) reported the element type for class components — ComponentElement`1 for every Component<T, TProps>(…), ComponentElement for Component<T>() — so winapp devtools logs and the overlay error badge could not name <Counter>. Mount-time and host-root render errors emitted no event at all, and neither did any render error an ErrorBoundary caught. This PR reports the component's type name on every path where a component's Render() throws, including inside a boundary, with exactly one event per throw.

Linked issue / spec

Fixes #1321

Spec 044 §6.2.1 (PII) is respected: payloads carry developer-authored type names only.

What changed

  • ComponentNames.For(Component? instance, Element? element) (internal, src/Reactor/Core/Diagnostics/ComponentNames.cs), precedence: the live instance's type → ComponentElement.ComponentType → the element type (FuncElement / MemoElement). Non-generic names are exactly Type.Name, as before; a generic component renders as ItemList<Int32> instead of ItemList`1. A nested generic shows only its own type arguments. Reflects only Name / GetGenericArguments, so it is AOT/trim-safe. Generic names are cached per type in a ConditionalWeakTable, so collectible or hot-reloaded types are never rooted.

  • RenderError uses that name, and is now emitted from:

    • the update path, which no longer depends on the Render keyword being on;
    • the three mount paths — Component, Func, Memo (new);
    • the root render catch blocks (component and render function) in ReactorHost and ReactorHostControl (new), and those hosts' outer catch when the root's effect flush throws (new). Reconcile / Cleanup failures have no failing component and are not reported;
    • inside an ErrorBoundary (new): each component catch gains a sibling clause, when (_errorBoundaryDepth > 0), that emits RenderError and rethrows unchanged. The throw site is the only place that knows which descendant threw, and the boundary does not emit again, so there is exactly one event per throw.

    Still RenderError (EventId 9, Errors keyword, Error level, same payload). Existing subscribers pick up the new emissions with no change.

  • ComponentRenderStart / Stop, EffectsFlush*, ComponentUnmount (including the ErrorBoundary rollback unmount from [Feature] Let apps replace the built-in error fallback that shows the full exception text #1291) and the reconciler's ILogger messages use the same helper, so every event agrees on the name. ComponentUnmount now computes the name only when its Lifecycle keyword is enabled.

  • SwallowedError is unchanged (its payload is category / operation / exception type; no call site passes a component).

  • Guide: source-mapping (template + compiled + diagram) — the component-name paragraph, the attribution diagram label/alt text, and the RenderError row. CHANGELOG.md [Unreleased] → Fixed entry. advanced guide: how the ETW event relates to RenderErrorHandler. Spec-044 swallowed-error ledger rows for every render-error catch this PR touches.

Test plan

  • New unit tests tests/Reactor.Tests/Diagnostics/ComponentNamesTests.cs (12). Mutation check: reverting to element naming / raw Type.Name fails 4 of the original 9; GenericName_IsBuiltOnceAndReused covers the per-type name cache.
  • New selftest fixtures tests/Reactor.AppTests.Host/SelfTest/Fixtures/RenderErrorNamesFixture.cs, subscribed to Errors only:
    • RenderErrorNames_ComponentTypeOnEveryPath — class/func/memo mount, update, ReactorHost root component + render function, ReactorHostControl root component + render function.
    • RenderErrorNames_ErrorBoundaryCatchIsReported — inside a boundary on mount (class, func, memo, and a throw that crosses an intermediate component) and on update: fallback shown, reported exactly once, by the thrower's name. Also an exception whose Message getter throws, and an explicitly thrown OutOfMemoryException on every boundary arm (class/func/memo mount and update).
  • dotnet run --project tests/Reactor.AppTests.Host --no-build -c Debug -p:Platform=x64 -- --self-test --filter "RenderError" — passed, 0 failures (at the merge 4075055bf), together with main's RenderErrorHandler_* and ErrorBoundary* fixtures.
  • Mutation check of the new selftest oracles: removing the FuncElement mount emission and the ReactorHostControl root-function emission fails RenderErrorNames_Mount_FuncComponentReported and RenderErrorNames_HostControl_RootFunc_Reported (2 failures); reading ex.Message in EmitRenderError fails RenderErrorNames_Boundary_ThrowingMessage_ReportedOnce; restoring the OOM/SO carve-out on the boundary arms fails all 4 boundary *OutOfMemory_ReportedOnce checks; limiting the report-and-rethrow arms to boundaries again fails the 4 non-boundary OOM checks. Restored afterwards.
  • dotnet test tests/Reactor.Tests -p:Platform=x64 — passed: 14435 total, 14371 succeeded, 64 skipped, 0 failed (at d8496be71, on origin/main 1e8c6b196). dotnet test tests/Reactor.DocPipeline.Tests -p:Platform=x64 — passed 479/479. SwallowedErrorAuditTests — passed 11/11.
  • dotnet build src/Reactor/Reactor.csproj --no-restore -c Release -p:Platform=x64 and dotnet build tests/Reactor.AppTests.Host --no-restore -c Release -p:Platform=x64 — passed, 0 errors (warnings-as-errors on). The full dotnet build Reactor.slnx --no-restore -c Release could not be completed locally: dotnet restore Reactor.slnx fails with NU1301 (TLS handshake to nuget.org) for the vs-reactor projects in this environment; relying on CI's Build solution job for that gate.
  • REACTOR_SELFTEST_SHARD=1/2 dotnet test tests/Reactor.SelfTests -p:Platform=x64 at 78a593d2e — passed: 930 total, 929 succeeded, 1 skipped (the intentional SelfTestVerdict_OnlySkips_PositiveControl), 0 failed.
  • No public API added; reactor.api.txt unchanged.
  • Merged origin/main (00cd48920: Let apps replace the built-in render-error fallback #1311 RenderErrorHandler, Add pr-lifecycle contributor skill #1346). Conflicts at every render catch site were resolved keeping both intents. Main's BuildInTreeFallback / ShowErrorFallback dispatch and !RenderErrorDispatch.IsPropagating(ex) filters are unchanged. RenderError is emitted before the handler runs, so it fires once per throw whatever the handler returns. The report-and-rethrow arms also let a declined propagation pass, since its throw site already reported it. New fixture RenderErrorNames_RenderErrorHandlerOutcomesReportOnce covers an app fallback (child and root), a handled Propagate(), and a declined Propagate() escaping through an ErrorBoundary; each is reported once, by name. Mutation: dropping the IsPropagating filter reports the declined case twice (FuncElement, NestedDecliningComponent). RenderError.ComponentName (the handler API) keeps main's values. API index regenerated with mur --regen-api: unchanged. Copilot's round on the merge raised 3 findings, fixed in 9ee41eaa4. (1) The [Feature] Let apps replace the built-in error fallback that shows the full exception text #1291 rollback ComponentUnmount used the old naming; it now goes through ComponentNames + the keyword guard, covered by RenderErrorNames_BoundaryRollbackUnmountNamesComponent. (2, 3) Host-root effect failures reached the handler with no RenderError; they now emit, covered by RenderErrorNames_*RootEffect* / *RootFuncEffect*. All 5 new checks fail with the fixes reverted. The docs scope the event to component Render()/effect failures, and the spec-044 ledger records the hosts' outer catches (LedgerSiteTotal 136 → 138). The next round, fixed in 0469209dc: (a) both hosts read _rootComponent after app code ran, so a root that Mount()s a replacement and then throws was attributed to the replacement. The root is now captured before Render() / FlushEffects(), and the same instance names the event and the handler's RenderError.ComponentName; covered by RenderErrorNames_*RootReplaced* (4 checks, all red on the mutant). (b) "Previously missed": ComponentNames cut an array argument down to its generic name (GenericList<List<int>[]> → GenericList<List>); it now formats element type and rank, covered by the unit test ArrayArguments_KeepTheirElementTypeAndRank (red on the mutant). Merged origin/main again (fix(datagrid): keep inline editor text aligned with the display cell #1349, Enable IDE0330 and move lock gates to System.Threading.Lock #1351): the CHANGELOG conflict was resolved by keeping both Fixed entries. The next round found that the func/memo mount arms hardcoded FuncElement / MemoElement, while those records are non-sealed and update/unmount name derived records by their own type. Fixed by routing them through ComponentNames.For, covered by RenderErrorNames_Mount_Derived*ElementNamed (red on the mutant). Release builds of src/Reactor, tests/Reactor.AppTests.Host and tests/Reactor.Tests are clean under the newly enabled IDE0330. The next round asked for generic derived element records to use the generic format (DerivedFunc<Int32>, not DerivedFunc1). Fixed in b585258and covered by the unit testGenericDerivedElements_UseTheGenericFormat(red on the mutant). Mergedorigin/mainagain (#1347, #1318) with no conflicts. #1318's new publicReactorHostInfo.RootComponentName` deliberately uses its own fully qualified format and is untouched.
  • Includes a one-line fix for a red main. At 22f267926, main's CI fails on src/Reactor/Core/Diagnostics/ReactorDiagnostics.Hosts.cs(305): error IDE0330. Diagnostics: enumerate Reactor hosts and their roots; tag component boundaries for inspectors #1318 added ReactorHostRegistry's object gate after Enable IDE0330 and move lock gates to System.Threading.Lock #1351 enabled IDE0330. Commit 3cc699958 makes it a System.Threading.Lock, the same change Enable IDE0330 and move lock gates to System.Threading.Lock #1351 made to every other gate. It's unrelated to this PR. Main then fixed the same break in Fix IDE0330 build break in ReactorHostRegistry #1356; the next merge (671fed2dc) took main's version of that file, so the one-liner merged away. After that merge: the API index was regenerated (unchanged), and the only lock statements this PR adds are test-local locks on List instances, not object gates, so IDE0330 is clean.
  • After the merge: dotnet test tests/Reactor.Tests -p:Platform=x64 passed 14579 total, 0 failed, 64 skipped (at 3cc699958). Release builds of src/Reactor, tests/Reactor.AppTests.Host and tests/Reactor.Tests are clean, and the RenderError / ErrorBoundary / HostDiagnostics selftests have 0 failures. dotnet test tests/Reactor.DocPipeline.Tests -p:Platform=x64 passed 493/493. Release src/Reactor builds clean. dotnet test tests/Reactor.SelfTests -p:Platform=x64 (full): 1785 total, 1777 passed, 5 skipped, 3 failed. The failures are Issue487_ScrollViewOffsetRestoredAfterRunMutation, ItemsView_Parking_PendingBringThenKeyedReset and ItemsView_Parking_VariableHeightReset; a clean origin/main 00cd48920 worktree fails them identically on this machine, so they are environmental and unrelated to this PR. CI is the gate.
  • Copilot review round 1 (3 comments): two said RenderError leaks ex.Message — not the case, the ReactorEventSource.RenderError sink writes string.Empty for the message (TASK-064); replied and noted it on EmitRenderError. One asked to cache generic names — done in 78a593d2e (ConditionalWeakTable per type), and both ComponentUnmount call sites now skip computing the name when the Lifecycle keyword is off. Round 2: "Previously missed" — CHANGELOG entry; added under [Unreleased] → Fixed. Round 3: the new catches were missing from the spec-044 swallowed-error ledger (docs/specs/044/swallowed-error-audit.md); recorded 12 sites (8 Keep / user-callback isolation fallback catches incl. the host roots, 4 PromoteEvent report-and-rethrow arms), distribution re-derived, LedgerSiteTotal 124 → 136. Round 4: don't read the virtual ex.Message on the error path (a throwing override would replace the original exception) — fixed, selftest added. Round 5 "Previously missed": the boundary arms' OOM/SO carve-out let an explicitly thrown OutOfMemoryException be recovered silently — carve-out removed (the boundary's own catch takes everything), selftests added; and the source-mapping diagram label + alt text still said Component.GetType().Name — updated in template, guide and both SVG copies. Round 6 "Previously missed": outside a boundary an explicitly thrown OOM/SO skipped the fallback arm and was recovered by the host's outer catch without an event — the report-and-rethrow arm now takes OOM/SO anywhere (they still propagate exactly as before); selftests cover class/func/memo mount and update. Round 7: Findings: None, nothing previously missed.
  • Pre-PR review with the repo's pr-review skill: 0 critical / 0 high. Four medium test-coverage gaps (func/memo mount, ReactorHost root render function, ReactorHostControl roots) fixed by the third commit. One low finding dismissed: "ComponentNames.For(Type) duplicates Reconciler.Mount.FriendlyTypeName" — FriendlyTypeName prints the declaring chain (Outer.Inner<T>) for copy-pasteable handler diagnostics, while component event names deliberately keep Type.Name for nested types so existing trace names stay byte-identical; sharing would need a mode switch that changes one or the other. Multi-model cross-check agreed.

Risk / breaking changes

  • Listeners now receive previously silent errors. RenderError also fires for render errors at mount time (class, function, memo), in the host root (ReactorHost and ReactorHostControl), and inside an ErrorBoundary (once, at the throw site, even though the app recovered and shows the fallback). Errors-keyword consumers will see more events.
  • The component name changes from ComponentElement / ComponentElement`1 to the component's type name. That is the fix, but greps keyed on the old string will stop matching.
  • Generic component names change from Foo`1 to Foo<Int32> in all component events. Non-generic names are byte-identical.
  • No new public API; same event, same payload shape.
  • Follow-up (not in this PR): the pending ComponentRendered event change should use the same ComponentNames helper; that will be folded into whichever of the two lands second.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Copilot was unable to run its full agentic suite in this review.

Copilot review overview

Review effort: Lite
Findings: 1 High severity · 2 Medium severity

Open (3)
What changed in this PR

This PR improves Reactor diagnostics by consistently reporting component names (not ComponentElement / ComponentElement\1) across render tracing, unmount, and RenderErroremission paths, including behavior insideErrorBoundary`.

Changes:

  • Introduces ComponentNames helper to compute stable, readable component names (including generic argument formatting).
  • Updates reconciler + hosts to emit RenderError consistently on mount/update/root, including when an ErrorBoundary catches and recovers.
  • Adds unit + self-test fixtures and updates documentation to reflect the new naming behavior.
File Description
tests/​Reactor.Tests/​Diagnostics/​ComponentNamesTests.cs Adds unit tests validating ComponentNames output across element/instance/generics/null cases.
tests/​Reactor.AppTests.Host/​SelfTest/​SelfTestFixtureRegistry.cs Registers new self-test fixtures covering RenderError name attribution paths.
tests/​Reactor.AppTests.Host/​SelfTest/​Fixtures/​RenderErrorNamesFixture.cs Adds end-to-end ETW subscription tests for RenderError naming (mount/update/root, and ErrorBoundary).
src/​Reactor/​Hosting/​ReactorHostControl.cs Emits RenderError for host-control root component/function paths using the new naming rules.
src/​Reactor/​Hosting/​ReactorHost.cs Emits RenderError for host root component/function paths using the new naming rules.
src/​Reactor/​Core/​Reconciler.cs Centralizes RenderError emission, switches diagnostics naming to ComponentNames, and reports errors inside ErrorBoundary at throw site.
src/​Reactor/​Core/​Reconciler.Mount.cs Aligns mount-time error reporting with update-time behavior (including ErrorBoundary path).
src/​Reactor/​Core/​Diagnostics/​ComponentNames.cs New helper to attribute diagnostics to component types rather than element carrier types.
docs/​guide/​source-mapping.md Documents updated component naming behavior across diagnostics events.
docs/​_pipeline/​templates/​source-mapping.md.dt Mirrors doc updates in the template used to generate the guide.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Reactor/Core/Reconciler.cs
Comment thread docs/guide/source-mapping.md Outdated
Comment thread src/Reactor/Core/Diagnostics/ComponentNames.cs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The user-visible diagnostics change needs the repository-required [Unreleased] changelog entry.

Review effort: Balanced
Findings: None

Resolved since last review (3)
Previously missed (1)

In code that hasn't changed since last review

Low severity Document diagnostics changes in CHANGELOG.md

docs/​guide/​source-mapping.md:82

This externally visible diagnostics change is missing from CHANGELOG.md. The repo requires notable changes under [Unreleased] in a Keep-a-Changelog bucket with the originating reference (CHANGELOG.md:3,13-24), and this change both adds previously silent RenderError emissions and changes names consumed by existing listeners. Please add a Fixed entry citing issue #1321 (and spec 044 §6.2.1).

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build metrics

Artifact sizes for 671fed2 vs the base branch (12d0816).

Packages (compressed .nupkg)

Artifact base PR Δ
Microsoft.UI.Reactor.nupkg 1.79 MB 1.80 MB +1.62 KB (+0.09%) ⚠️
Microsoft.UI.Reactor.Advanced.nupkg 493.4 KB 493.3 KB -13 B (0.00%) ≈
Microsoft.UI.Reactor.Devtools.nupkg 288.2 KB 288.2 KB +5 B (0.00%) ≈

Assemblies in Microsoft.UI.Reactor

Artifact base PR Δ
Reactor.Analyzers.dll 386.0 KB 386.0 KB +0 B (0.00%) ≈
Reactor.dll 2.52 MB 2.52 MB +1.50 KB (+0.06%) ⚠️
Reactor.Localization.Generator.dll 16.0 KB 16.0 KB +0 B (0.00%) ≈
Reactor.Wrappers.Abstractions.dll 10.5 KB 10.5 KB +0 B (0.00%) ≈
Reactor.Wrappers.Generator.dll 99.5 KB 99.5 KB +0 B (0.00%) ≈

Assemblies in Microsoft.UI.Reactor.Advanced

Artifact base PR Δ
Reactor.Advanced.dll 1.00 MB 1.00 MB +0 B (0.00%) ≈

Assemblies in Microsoft.UI.Reactor.Devtools

Artifact base PR Δ
Microsoft.UI.Reactor.Devtools.dll 788.0 KB 788.0 KB +0 B (0.00%) ≈

✅ smaller / ⚠️ larger / ≈ within noise. Sizes come from a Release dotnet pack on the CI runner: packages are the compressed .nupkg download size, assemblies the uncompressed DLL inside it.
workflow run.

RenderError reported the ELEMENT type for class components: "ComponentElement`1"
for every Component<T, TProps>(...) and "ComponentElement" for Component<T>(),
unless the Render keyword was also enabled (the update path only borrowed the
real name from the render-span bookkeeping). The mount path and the hosts' root
render emitted no RenderError at all, so a component that throws on its first
render, or a throwing root, was invisible to an Errors-only listener.

- ComponentNames.For(instance, element): live instance type, else
  ComponentElement.ComponentType, else the element type (FuncElement /
  MemoElement). Non-generic names are unchanged (Type.Name); generic components
  render "ItemList<Int32>" instead of "ItemList`1". Type names only (PII-safe).
- RenderError now uses it on the update path and is emitted on the mount paths
  (Component / Func / Memo) and for ReactorHost / ReactorHostControl roots.
- ComponentRenderStart/Stop, EffectsFlush and ComponentUnmount, plus the
  reconciler's ILogger messages, use the same name.
- SwallowedError carries no component name (category / operation / exception
  type only): unchanged.
- Unit tests for the naming (mutation-checked); selftest fixture
  RenderErrorNames_ComponentTypeOnEveryPath for mount/update/root emission;
  source-mapping guide.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A component render error inside an ErrorBoundary propagates to the boundary,
which logs and renders its fallback. The component's own catch is filtered on
_errorBoundaryDepth == 0, so no RenderError was emitted: the user saw a
fallback while an inspector (overlay error badge, devtools logs) saw nothing.

- Each component render catch (update path, and Component / Func / Memo mount)
  gains a sibling clause for _errorBoundaryDepth > 0 that emits RenderError with
  the failing component's ComponentNames name and rethrows unchanged. The throw
  site is the only place that knows which descendant threw; the boundary does
  not emit again, so it is exactly one event per throw.
- Same event (RenderError, EventId 9, Errors, Error level, same payload): every
  existing subscriber picks it up with no change, and the event's meaning (a
  component's Render() threw) is unchanged.
- Selftest fixture RenderErrorNames_ErrorBoundaryCatchIsReported (mount and
  update inside a boundary, once by name); source-mapping guide row.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…h hosts' roots

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…racing is off

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

🧪 Merged coverage

Coverage for 671fed2 vs the base branch (12d0816) — unit + selftest merged.

Metric base PR Δ
Line 86.52% 86.65% +0.13 pp ✅
Branch 77.36% (967/1250) 77.44% (968/1250) +0.08 pp ≈

✅ higher / ⚠️ lower / ≈ within noise. Δ is in percentage points; coverage is unit + selftest merged (Debug x64) on the CI runner. Cobertura reports attached to the workflow run as artifacts.

…rror (#1321)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The four new broad exception catches must be added to the repository’s required spec-044 audit ledger.

Review effort: Balanced
Findings: 2 Low severity

Open (2)

Comment thread src/Reactor/Core/Reconciler.Mount.cs Outdated
Comment thread src/Reactor/Core/Reconciler.cs Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Error emission unnecessarily evaluates Exception.Message, which can disrupt recovery for custom exceptions.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (2)

Comment thread src/Reactor/Core/Reconciler.cs Outdated
The sink redacts the message, and an app exception whose Message override
throws would otherwise replace the original exception on the
ErrorBoundary rethrow path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Boundary-caught fatal exceptions can still recover without emitting RenderError, and the source-mapping alt text remains stale.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (4)

In code that hasn't changed since last review

Medium severity Boundary-caught OutOfMemoryException remains invisible to Errors listeners

src/​Reactor/​Core/​Reconciler.Mount.cs:871

An ErrorBoundary catches every Exception in MountErrorBoundary, including an explicitly thrown OutOfMemoryException, but this filter bypasses the only throw-site RenderError emission for those exceptions. The boundary then renders its fallback, so the error is handled yet remains invisible to Errors listeners, contradicting the new “including errors an ErrorBoundary catches” contract. This boundary arm should report every exception that the enclosing boundary will catch (or the boundary itself must be narrowed consistently).

Medium severity OutOfMemoryException bypasses boundary-caught render error emission

src/​Reactor/​Core/​Reconciler.Mount.cs:919

The enclosing ErrorBoundary catches every Exception, but this filter skips RenderError for an explicitly thrown OutOfMemoryException before the boundary recovers with its fallback. As a result, this function-component mount path still has a boundary-caught render error with no event, contrary to the documented behavior.

This issue also appears on line 967 of the same file.

Medium severity Update boundary misses RenderError for OutOfMemoryException

src/​Reactor/​Core/​Reconciler.cs:2173

UpdateErrorBoundary catches every Exception, so an explicitly thrown OutOfMemoryException bypasses this emission and is nevertheless recovered by the boundary. That produces no RenderError for a boundary-caught update error, contrary to the new event contract; this throw-site arm should cover the same exception set as the enclosing boundary.

Low severity Update image alt text to reflect ComponentNames formatting

docs/​_pipeline/​templates/​source-mapping.md.dt:62

The image alt text immediately below still says component attribution flows via Component.GetType().Name, which would produce the raw Foo`1 form this paragraph now says is not emitted. Update the template to name the ComponentNames formatting path, then regenerate the compiled guide so the accessible description matches the documented behavior.

…ibution alt text

The report-and-rethrow arms excluded OutOfMemoryException and
StackOverflowException, but the boundary's own catch takes everything, so an
explicitly thrown OOM was recovered without a RenderError. The source-mapping
diagram and its alt text still named Component.GetType().Name.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…arguments

Capture the root component before app code runs in both hosts, so a root that
Mount()s a replacement from Render() or an effect and then throws is reported
(on RenderError and to the RenderErrorHandler) as itself, not the replacement.
ComponentNames now formats array arguments (List<Int32>[]) instead of cutting
them to the bare generic name.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The reporting paths are consistent, comprehensively tested, and synchronized with the documentation and error audit.

Review effort: Balanced
Findings: None

Resolved since last review (2)

Resolves the CHANGELOG conflict with #1349 by keeping both Fixed entries.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Mount events hardcode function and memo base names, causing inconsistent diagnostics for derived element records.

2 open findings

🧠 Review effort: Balanced

Comment thread src/Reactor/Core/Reconciler.Mount.cs Outdated
Comment thread src/Reactor/Core/Reconciler.Mount.cs Outdated
FuncElement and MemoElement are non-sealed; the mount arms hardcoded the base
names while update and unmount use ComponentNames.For, so a derived record had
two names. Route the mount arms through the helper too.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Generic derived function and memo elements still produce raw arity-based diagnostic names.

1 open finding
2 resolved since last review

🧠 Review effort: Balanced

Comment thread src/Reactor/Core/Diagnostics/ComponentNames.cs Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
main is red at 22f2679: #1318 added ReactorHostRegistry's object gate after
#1351 enabled IDE0330, so the Release build fails. Same one-line change #1351
applied to every other gate.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation is internally consistent, thoroughly tested, and synchronized with diagnostics documentation and auditing.

0 open findings

1 resolved since last review

🧠 Review effort: Balanced

…-component-name

# Conflicts:
#	src/Reactor/Core/Diagnostics/ReactorDiagnostics.Hosts.cs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Core exception routing changes span multiple reconciler and host recovery paths, warranting final human review despite strong coverage.

0 open findings

🧠 Review effort: Balanced

@azchohfi
Alexandre Zollinger Chohfi (azchohfi) merged commit b507677 into main Oct 7, 2026
46 checks passed
@azchohfi
Alexandre Zollinger Chohfi (azchohfi) deleted the azchohfi-render-error-component-name branch October 7, 2026 22:46
Alexandre Zollinger Chohfi (azchohfi) added a commit that referenced this pull request Oct 7, 2026
…ComponentNames

Keeps #1323's RenderError emission (before the fallback, including ErrorBoundary-caught throws) alongside ComponentRendered: the boundary arms main added now also report the render. Folds in the cross-dependency change: ComponentRendered names components with ComponentNames.For (generic classes as Foo<Int32>, FuncElement/MemoElement fallbacks), for child components and host roots, so it agrees with RenderError. Adds ComponentRendered_GenericNames_MatchComponentNames.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Alexandre Zollinger Chohfi (azchohfi) added a commit that referenced this pull request Oct 7, 2026
Conflicts kept both sides: CHANGELOG entries; NeedsTag keeps the Native AOT
call-site-only skip and adds #1318's component-boundary tag; the generator test
stub has both the static-info and root-mount-site members; one combined
InternalsVisibleTo comment. API index regenerated. K's tests now pass the
column that SourceLocation requires positionally.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Alexandre Zollinger Chohfi (azchohfi) added a commit that referenced this pull request Oct 7, 2026
…component names

Folds in the integration follow-up (2495eb4b7) now that #1318 and #1323 are
on main: a root render function's hooks= resolves from its recorded mount
site, and owner/mounts/root names forward to ComponentNames.For. Also adopts
System.Threading.Lock for the static-facts gate (IDE0330, now enforced in
Release).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Alexandre Zollinger Chohfi (azchohfi) added a commit that referenced this pull request Oct 14, 2026
…ohfi-reactor-component-diagnostics

- Hosts: keep #1328/#1323's root lifecycle (RetireRoot, deferred
  retirement, _releaseReplacedTreeOnNullRender, RootRenderDiagnostics)
  alongside the diagnostics rendered-root publication. The published root
  is snapshotted at the start of the pass (beside _renderingRoot), so a
  Mount() from the root's own Render() does not relabel the tree that pass
  commits; RetireRoot clears it once the root is retired (immediately or
  in the deferred pass), so a retired root is never described or written.
- Selftests: the remount gap now describes no root (the old one is
  retired at Mount); new mid-pass Mount case; mutation-checked.
- SourceLocation's new required ColumnNumber in a unit test.
- CHANGELOG keeps both entries; API index regenerated (unchanged).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Alexandre Zollinger Chohfi (azchohfi) added a commit that referenced this pull request Oct 21, 2026
… control, declared names, hook names (Native AOT) (#1342)

* Stamp the call-site column into SourceLocation

The source-map interceptor generator already knows the exact column of each
DSL call; the runtime SourceLocation only exposed file + line, so an inspector
could not tell Row(Button("a"), Button("b")) apart or anchor a comment on
the exact call.

- SourceLocation gains a non-positional int ColumnNumber { get; init; }
  (1-based, 0 = unknown) and a (FilePath, LineNumber, ColumnNumber) ctor.
  The 2-arg ctor, Deconstruct, ToString and ToShortString are unchanged and
  the struct stays 16 bytes.
- The generator stamps the column of the invoked method's name (the open
  paren's when name and paren sit on different lines, because the line
  follows the paren for [CallerLineNumber] parity), and the argument
  expression's column for argument-position stamps. Column takes part in the
  incremental-model equality so a sideways-only edit regenerates.
- Generator driver tests and live column tests (oracle = the test file's own
  text); spec 010 amendment, source-mapping guide, API index.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Add ComponentRendered event with render reason and instance id

Inspectors (winapp devtools "highlight updates", why-did-render) need to know
when each component renders and why. The existing ComponentRenderStart/Stop
pair is Informational timing only: it skips mount and the host root, reports
just "self"/"parent", and carries no instance identity, so an overlay cannot
tell which on-screen component to flash.

- ReactorEventSource.ComponentRendered, EventId 40, Level Verbose, keywords
  Render | RenderDetail (new, 0x4000). Payload: componentName (CLR type name),
  componentId (process-unique long), reason (mount | state | props | context |
  parent | hotReload | forced), elapsedMicroseconds. Written with
  WriteEventCore (no boxing/params array). No PII: type names and fixed tokens.
- Emitted from the reconciler's mount and update paths and from both hosts'
  root render. Reasons come from the existing memo/force/self-trigger state.
- ReactorTrace.GetComponentControl(long) / TryGetComponentId(UIElement, out
  long) resolve ids to the component's wrapper (or the host's root content).
  The registry is weak, populated only while the event is enabled, and
  dropped on unmount so a pooled wrapper is never misattributed.
- Disabled cost: one EventSource.IsEnabled check per component render
  (measured ~1 ns, 0 B), plus a long on each ComponentNode.
- Unit tests for the wire contract, keyword/level gating, classification and
  registry; a selftest fixture for the reconciler wiring; diagnostics and
  perf-instrumentation docs; API index.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Drop forced GC and Path.Combine from the ComponentRendered/column tests

Prune is now exercised by expiring a weak entry through a test hook rather
than GC.Collect(), which also makes it deterministic; ColumnTests joins a
fixed relative path instead of Path.Combine with segments.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Share host-root ComponentRendered bookkeeping; gate adoption tracking

ReactorHost and ReactorHostControl now hold one RootRenderDiagnostics
(id, first-render flag, Reset/TraceRendered/TrackContent) instead of
parallel fields and methods, so the two hosts cannot drift. The
realized-replacement adoption path only touches the registry while
ComponentRendered is enabled; the next traced render re-tracks the id.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Report ComponentRendered on every error path; changelog

- A component whose Render() throws inside an ErrorBoundary now reports the
  render (mount and update paths) before the exception reaches the boundary.
- A host pass that fails after the root rendered maps the root id to the
  error panel the outer catch installs, in both hosts.
- Retargeting an id clears the previous control's reverse slot.
- CHANGELOG entry for SourceLocation.ColumnNumber, ComponentRendered and the
  ReactorTrace lookups (issue #1326).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Never leave a ComponentRendered id on a control that is off screen

- Mount paths map a component's id to its wrapper only after the subtree has
  mounted; a descendant that throws into an enclosing ErrorBoundary discards
  the wrapper without unmounting it.
- Reconciler.ForgetComponentDiagnostics drops every tracked component's id;
  Reconciler.Dispose and both hosts' ShowErrorFallback (which replace the
  tree without unmounting it) call it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Report reason context for function components whose context changed

A function component has no memo gate, so it reported parent even when a
context value it consumes is what changed. Classify that case as context,
computed only while ComponentRendered is traced.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Attribute a hot-reload hook-order retry to hotReload, not forced

When a hot-reload edit changes the root's hook order, the host resets the
root and retries outside the hot-reload pass, so ComponentRendered reported
that retry (root and children) as forced. The host now marks the pending
force pass as a hot-reload retry on the reconciler (diagnostics only; render
behaviour is unchanged) and both the root and child classification honour it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Tighten ComponentRendered reasons and error coverage; regenerate docs

- Trace a render that threw into an ErrorBoundary for every exception the
  boundary can catch (drop the OOM/SO exclusion on the trace-and-rethrow
  clauses).
- A propless component whose consumed context changed reports context, not
  parent; props components keep props-over-context precedence.
- Function-component updates read the event gate once.
- SourceLocation stays 16 bytes on 64-bit only; x86 grows from 8 to 12.
  The size test and spec 010 say so.
- source-mapping frontmatter and diagram alt text mention the column;
  architecture-overview and reconciliation recompiled for the moved snippet.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Forget a component id on DetachReactorState; show the column in the diagram

DetachReactorState, the full-detach teardown, now drops the control's
ComponentRendered id in both directions like unmount does. The
source-mapping attribution diagram labels per-element attribution as
file:line:column and shows a column in the CallSite example.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Reuse the ComponentRendered gate snapshot on the update path

The function-component context classification reads the event gate into
traceEnabled; the render path now reuses that snapshot instead of reading
the gate again, so a listener enabling between the two reads cannot turn a
context-driven render into one reported as parent.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Keep the host-root id honest across root swaps and tracing off

- ReactorHost.Mount(Func) clears the component root, as ReactorHostControl
  does; the render loop checks the component root first, so the old root kept
  rendering under the new root's fresh mount id.
- The root's content mapping is updated on every content change: tracked while
  ComponentRendered is enabled, dropped while it is off, so it never names a
  control that left the screen.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Use a using declaration for the root-mapping fixture's subscription

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Run the outgoing component root's cleanups when switching to a function root

Mount(Func) now retires the previous component root's RenderContext (its
effect cleanups run, as on Dispose) before clearing it, in both hosts;
otherwise a root UseEffect subscription or timer outlived the switch and was
no longer reachable by Dispose.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Roll back boundary-discarded mappings; retire any outgoing root on remount

- Mounts inside an ErrorBoundary record the component ids they map; a boundary
  that catches (on mount or update) forgets every mapping made since it
  started, so a healthy sibling mounted before a throwing one does not stay
  mapped to a discarded wrapper.
- Both hosts retire whichever root is active (component or function context:
  cleanups run, both slots cleared, diagnostics reset) before either Mount
  overload installs the new one.
- Document that a host root reports state for host-requested re-renders too.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Keep the active root alive when it is re-mounted

Mount(component) with the instance already mounted now just requests a
render. Retiring it ran its effect cleanups and then reused the same
RenderContext, whose unchanged effects would never be scheduled again.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Reset a retired root component's hooks, not just its cleanups

RetireRoot now resets the outgoing component's hook state
(RenderContext.ResetHookState: cleanups run and hook cells are dropped), so
mounting that caller-owned instance again later is a fresh mount: its effects
are scheduled again and old state does not survive. ResetForHotReload
delegates to the same method.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Report hot-reload hook-order attempts and null-returning roots

- A Render() aborted by a hot-reload hook-order change (child in the
  reconciler, root in either host) now emits ComponentRendered before the
  reset; the retry reports itself, timed on its own.
- A root whose Render() returns null is reported too (nothing to reconcile,
  but it rendered); a host with no root still reports nothing.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Report a throwing render before its fallback, and for every propagated exception

- The fallback catch emits ComponentRendered before building the error
  fallback, so the fallback's time is not counted and a fallback that throws
  cannot swallow the event.
- The trace-and-rethrow clause now covers every exception the fallback catch
  does not handle (an ErrorBoundary-bound one, or a fatal one propagating to
  the host), on mount and update.
- The update reason is computed once per render.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Track the root's hot-reload retry in RootRenderDiagnostics

Both hosts mark the root's hook-order retry on RootRenderDiagnostics, so the
root reports it as hotReload whether or not the host's hot-reload pass is a
forced one. ReactorHost (whose pass is forced and whose force flag is still
pending) keeps also marking the reconciler so its children follow;
ReactorHostControl's passes are not forced, so it no longer sets that
reconciler flag, whose comment overstated what it did there.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Publish ReactorSource diagnostics on controls; declared names and hook names

Lets an out-of-process inspector (WinUI DevTools over XamlDiagnostics) show
Reactor's component tree, call sites, keys, declared names and hook names with
no managed agent in the app, so Native AOT apps can be inspected too.

K: ReactorDiagnostics.SourceProperty (attached string DP, "ReactorSource")
- Set on EVERY realized control in diagnostics mode (mount, update, skip-arm
  call-site refresh, hot-reload migration, component wrappers, host roots),
  not only the controls Reactor tags. Value v1:
  v=1|at=path:line[:col][|rel=root|0]|owner=C|element=Kind[|mounts=C][|root=C]
     [|key=k][|name=n][|hooks=i:name@line;...]   (% -> %25, | -> %7C)
- at= is never an absolute developer path (XAML ms-appx parity): relative to
  the project directory, else to the solution root (rel=root), else the file
  name only (rel=0); deterministic /_/ paths are kept. CallSite is unchanged.
- Gating: Reactor.DevtoolsSupport switch AND REACTOR_DIAGNOSTICS=1. Call sites
  guard on the new cached feature switch ReactorFeatures.DevtoolsSupported,
  so trimmed/AOT builds without it carry no publishing code (AOT Hello-World
  trim assertions extended; positive control with the switch on keeps it).
- $(ReactorDiagnostics) MSBuild property: default true in Debug; true turns
  the switch on (Trim=true) and implies ReactorSourceMap; explicit settings,
  and a project-declared switch over the Debug default, win.

I: declared names and hook names (source-map generator, Debug/source-map only)
- The generator records the identifier an element is assigned to (locals,
  fields, properties, assignments, expression-bodied members/local functions,
  through fluent chains) -> SourceLocation.DeclaredName and name=.
- Per class Render() / render function: ordered Use* calls with the variable
  they are stored in and their line -> hooks=; slot indices exact for
  single-slot built-ins (runtime-verified table) and straight-line custom hooks
  in the project, '?' after unknown or conditional hooks.
- Emitted as a module initializer that registers a fill callback; tables are
  built lazily on first lookup.

Tests: generator snapshots (names, hooks, roots), live source-map tests,
format/escaping/relative-path/gating unit tests, hook slot table guard,
build-targets evaluation tests, ReactorSource selftest fixture (not run).
Docs: diagnostics, source-mapping guide; threat model §7.7.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* ReactorSource: fix lost owners after GC; publish only when the value can change

Measuring diagnostics mode (K) on a 1k-50k control benchmark found a
correctness bug and most of its cost in the publisher's per-control side table.

Bug: the last published owner/value lived in a ConditionalWeakTable keyed by
the managed UIElement. CsWinRT wrappers are collected and re-created for the
same native control across GCs, so entries vanished and a shallow-skip
re-publish (call site moved) wrote the value without |owner=. Measured: after
the benchmark's re-render series only 3,078 of 9,999 controls kept owner=
(1k: 306/990); with a forced GC before each re-render it showed after 2.

- No per-control state. Skip arms take the owner from the reconciler's current
  owner (they run inside the parent's reconcile, like updates).
- The update path re-publishes only when the value can change:
  IdentityChanged = call site, key, element kind or component type differ (or
  the control was replaced). An ordinary re-render builds and writes nothing.
- The host root keeps its last value on the reconciler (one per host).
- Values are cached per shape (site, owner, kind, mounts, root, name, hooks):
  key-free controls share one string; keyed ones are one concat. Format() is
  split out and pinned byte-for-byte to the v1 grammar.

Effect (Native AOT, 50k nested, delta vs no-K, per control): managed heap
559 -> 372 B; mount allocations 1,403 -> 610 B; allocation per unchanged
re-render 1,158 -> 300 B; re-render with changed call sites 27 -> 10 us.
Switch-off (b) and switch-on/env-unset (c) builds remain within noise of the
baseline; the trimmer still removes all of it when the switch is off (mstat).

Tests: ReactorSourcePublisherTests (identity rules; cached Format equals the
direct Build for plain/keyed/escaped/no-site/no-owner/column-0/absolute
shapes; shared instance for unkeyed values); selftest skip-republish owner.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Source map: share the call-site-only extras bucket per call site

Stamping (REACTOR_SOURCEMAP=1, and diagnostics mode, which implies it) gave
every element a fresh ElementExtras bucket (~150 B) holding only its
CallSite, retained for every realized control. The bucket is an immutable
record, so an element whose only extra is its call site now takes a shared
instance cached per SourceLocation (bounded by the app's call sites; the cache
lives in a holder class, so an app that never stamps never allocates it).
Adding any behavioral extra still copies the bucket.

Effect (Native AOT, 50k nested, per control, delta vs no stamping):
stamping-only heap 354 -> 202 B; diagnostics mode heap 372 -> 220 B and
allocation per unchanged re-render 300 -> 142 B.

Docs: diagnostics guide states the measured cost of diagnostics mode.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* ReactorSource: allocate the value cache on first use

Every guarded reconciler call site reads ReactorSourcePublisher.IsEnabled, which runs the type's static initializer even when REACTOR_DIAGNOSTICS is unset (the Debug default). The value cache (a ConcurrentDictionary) was a static field there, so a Debug app with the switch on and publishing off retained ~14 KB it never used (measured: flat 1k heap after mount +1.9% vs baseline, now +0.0%). It now lives in a holder class initialized on first publish.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* ReactorSource: skip the call-site-only element tag under Native AOT

In diagnostics mode every stamped element was tagged (ReactorState + its
attached DP) only so a managed inspector could read its call site back. No
managed agent can load into a Native AOT process, so there the tag is pure
cost: measured ~0.2 KB managed and ~0.8 KB native per control.

- NeedsTag no longer tags an element whose only extra is its call site when
  Reconciler.SkipsCallSiteOnlyTags: the Reactor.DevtoolsSupport switch,
  REACTOR_DIAGNOSTICS=1, and no managed agent
  (!RuntimeFeature.IsDynamicCodeSupported, decided once; settable for tests).
  Such a control is in exactly the state an unstamped build leaves it, which
  every functional tag reader already handles. Forced tags (decorators,
  wrappers, dialogs, lists, Win2D, generated wrappers) and tags for
  callbacks / keys / behavioural extras / reference modifiers are untouched.
- The spec-010 shallow-skip refresh allocates only via NeedsTag
  (RefreshCallSiteTagOnSkip) instead of forcing a tag.
- ReactorSourceMap.GetSource resolves an untagged control from its published
  value. Each value shape records its full SourceLocation against its at=
  text, so it returns the same SourceLocation the tag gave. When two call
  sites share the same at= text (same relative path from two assemblies),
  that control is tagged after all, so GetSource stays exact.
- JIT, Debug, and builds without the switch are unchanged; the trimmer still
  removes everything.

Measured (Native AOT, 50k controls, diagnostics on, per control vs no K):
managed heap 221 -> 20 B; private bytes 1,512 -> 713 B; mount allocations
458 -> 199 B; mount time 9.8 -> 3.3 us. Switch off / switch on with
REACTOR_DIAGNOSTICS unset stay within noise of the baseline.

Tests: a selftest runs one tree through both paths and requires GetSource to
agree on every control after mount and after a re-render that moves call
sites (43 controls, 30 located; 11 tags instead of 30, 19 resolved from the
published value). Mutation-checked: no fallback, or no republish on skip,
fails it. Unit tests: at= key parsing, published value -> full location,
ambiguity, mode predicate.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* ReactorSource: address review findings

- Security: only the SDK's PathMap roots (/_/, /_<digits>/) count as
  deterministic, non-absolute paths. Any other /_... directory (/_work/...)
  is absolute and is published as its file name with rel=0.
- ItemsRepeater adoption moves a fresh component subtree into the realized
  wrapper; the wrapper now also takes the replacement's published value, so
  it reports the new key.
- AOT GetSource fallback: a collision of at= texts no longer erases the first
  mapping. Controls published after a collision keep their tag; an untagged
  control with that text was published before it, for the first site.
- Tests: published name= and hooks= on live controls; adoption republish
  (mutation-checked); /_work paths; collision keeps the first site.
- Docs: CHANGELOG entry; reactor-build-and-check skill documents
  ReactorDiagnostics and the Debug default; unambiguous rel= in the
  SourceProperty XML doc; threat model states the PathMap-root rule.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* ReactorSource: review round 2 fixes

- Security: a path rooted on the current drive (\Users\me\App.cs) is
  absolute too; it is published as its file name with rel=0.
- Owner: the host's root owner is set only for its own render pass and
  cleared after it. Rows an ItemsRepeater realizes or reuses during layout
  run Reconcile outside any render, and used to pick up a stale root owner
  (owner= is now omitted for them, as documented).
- Tests: rooted-path cases; a scroll selftest with keyed component rows
  (pass-2 adoption during layout), mutation-checked (all 20 reused rows
  reported the stale owner without the fix).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* CHANGELOG: link issue #1341

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* ReactorSource: address Copilot review

- Generator: hooks are ordered by where the call ends, because C# evaluates
  arguments first: in UseState(UseMemo(...)) the inner hook takes the
  earlier slot. Regression test for a nested hook.
- A Memo(key, ...) host root now gets root= (and hooks=): the host adds the
  root fields to the value its factory output published, without running
  the factory again (ReactorSourcePublisher.WithRoot). Unit test (matches
  Format for a root) and a selftest (factory ran once).
- The host-root publish keeps no reference to the root control. It compares
  against the control's current value, so a disposed host's subtree is not
  retained.
- Static source-map table: published before the pending marker is cleared
  (volatile), so a lock-free reader never sees a cleared marker with the old
  or missing table.
- Reactor.targets: a final ReactorDiagnostics=false turns the switch off even
  over a project-declared Reactor.DevtoolsSupport=true (scaffolded Debug
  apps) or when set in the project body after the import. This mirrors
  -p:ReactorDiagnostics=true beating a project-declared false. Targets tests
  cover both import orders (mutation-checked).
- Docs: recompiled architecture-overview (its ElementFactory snippet moved)
  and diagnostics; the build-and-check skill notes the opt-out precedence.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address Copilot review round 2 on ReactorSource

- Reactor.targets: a ReactorDiagnostics=true set in the project body after an
  early import now also implies ReactorSourceMap (late target + analyzer item
  condition evaluated against the final values).
- Hook names: the generator records every Render() override (hook-free ones
  too), and GetComponentHooks walks base types, so an inherited Render reports
  the declaring base's hooks instead of nothing.
- Hook tables are keyed by (assembly, full name), so equal type names in
  different assemblies no longer collide; RegisterStaticInfo takes the assembly.
- Pool return and detach clear the published ReactorSource value, so the
  Native AOT GetSource fallback cannot report a recycled control's old element.
- diagnostics doc links the threat model by absolute URL (mkdocs --strict).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address Copilot review round 3 on ReactorSource

- Reactor.targets: the Debug default no longer writes ReactorDiagnostics at
  evaluation, so a project-body ReactorDiagnostics=true after an early import
  stays an explicit opt-in that beats a project-declared switch-off; items
  apply the default and _ReactorApplyLateDiagnosticsOptIn fills the property.
- Hook slots: component-extension hooks (this.UseElementFocus()) are hooks;
  abstract, overridable and body-less custom hooks are an unknown slot count.
- Root swap: mounting a different root over kept content rewrites owner= on the
  root-owned controls the in-place update did not republish (both hosts).
- Static info: a location two assemblies claim with different names or
  render-function hooks is unknown instead of last-writer-wins.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address Copilot review round 4 on ReactorSource

- Native AOT tag skip keeps the tag of a stamped element whose V1 handler tears
  down child slots the generic unmount walk does not reach (NavigationView pane
  slots, item hosts): unmount dispatches to that handler through the tag.
- ReturnControl clears the published value even when the pool declines the
  control (pooling disabled), so GetSource's fallback forgets it too.
- Static info: one fingerprint per source file over every call site's name or
  its absence and every render function's hooks or their absence; a path two
  assemblies stamp with different fingerprints has no attributable facts.
- Root swap: the owner rename also walks flyout content / pass-through, popup
  children, a showing dialog's content and cached navigation pages.
- Keyed-memo roots: hooks= the host added on an earlier pass is replaced or
  dropped when a different root is mounted.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address Copilot review round 5 on ReactorSource

- Native AOT tag skip reads the handler's ChildrenForUnmount (descriptor item
  hosts and items-binder strategies are hidden from Children), so a stamped,
  callback-free ComboBox / Pivot with component items keeps its teardown tag.
- Root-owner rename visits a node's overlay edges before stopping at a
  component wrapper (Flyout(Component<T>(), body)), and walks logical children
  too (generic content walk, the handler's live strategy children, items, tabs),
  deduplicated, so unrealized template content such as a closed flyout's
  Button content is renamed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Gate the new ReactorSource fixtures on what the host provides

The AOT and packaged selftest hosts are not source-mapped: nothing is stamped
(so the tag-skip teardown scenario does not exist there) and there is no
static hook table (so a keyed-memo root has no hooks to replace). Skip those
checks there, as the existing both-paths fixture already does.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address Copilot review round 6 on ReactorSource

- Root-owner rename stops at an embedded ReactorHostControl: its island is
  another host and names its own root.
- Keyed-memo roots: hooks the host added before are only removed from a value
  that still carries the host's root fields, never from a fresh output whose
  own hooks happen to match.
- ContentDialog: the visible WinUI dialog carries the element's published
  value too (mirrored when the placeholder is published, copied when a dialog
  is created later), and the rename walk renames it.
- Native AOT tag skip keeps the tag for any stamped element whose handler is
  hand-written (its Unmount may hold imperative teardown); descriptor handlers
  are unaffected. OwnsChildTeardown is now OwnsTeardown.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address Copilot review round 7 on ReactorSource

- Root publication unwraps ModifiedElement before its kind checks, so a
  modifier-wrapped keyed-memo root keeps describing its realized output.
- Both root-publication paths (plain and keyed-memo) mirror the final value
  onto a live ContentDialog, so the visible dialog follows a root swap; the
  mirror keys on the placeholder's live dialog, not on re-running a factory.
- ReactorSourceMap.Enabled starts on under the diagnostics launch opt-in
  (REACTOR_DIAGNOSTICS=1 in a build with Reactor.DevtoolsSupport), so elements
  built before the first host, such as a page's field initializers, are
  stamped. The read stays behind the feature switch.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* docs: diagnostics launch opt-in also starts source mapping

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Root-owner rename never enters an embedded host, even as the root's content

A root that returns XamlHost(() => island) makes the island its content
control; the rename walk now stops at any ReactorHostControl after renaming it
and visiting its decorator-owned overlays, so the island's own annotations are
never rewritten.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address Copilot review round 9 on ReactorSource

- A per-host RegisterType callback that forwards to a child returns the
  child's control: its published value stays the child's on mount and update
  (decided from the value, so it holds where neither element is tagged), and
  Native AOT GetSource matches JIT.
- A ContentDialog body mounted outside a render (a deferred Loaded open) is
  mounted under the owner its placeholder was published with.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Forwarding RegisterType roots keep the child's source fields

A host root that dispatches to a per-host RegisterType registration (no V1
handler for the type, as in Mount) and returns a control describing another
element gets only the host-root fields added (WithRoot), like a keyed-memo
root, so Native AOT GetSource matches JIT.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address Copilot review round 11 on ReactorSource

- Forwarding RegisterType: the child's published value is kept only when the
  child holds (or, stamped, would hold) the JIT tag; for an unstamped,
  untagged child the registration's location is published, as JIT's
  registration tag reports it.
- Root-owner rename also walks context-flyout and attached-flyout content and
  a Reactor-mounted tooltip, before stopping at component or host boundaries.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* GetSource names a forwarded child when a forwarding registration tags itself

In the Native AOT skip mode a forwarding RegisterType registration that needs
a tag (a key, an unmount callback) tags the control with itself because the
stamped child is untagged; JIT keeps the child's tag. GetSource now answers
from the published value (the child's) when it describes another element
than the tag, matching JIT.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Isolate the process-wide Reactor.DevtoolsSupport switch in tests

- TestSetup captures the configured switch and initializes the cached gates
  (ReactorFeatures.DevtoolsSupported, ReactorSourcePublisher.IsEnabled,
  ReactorSourceMap.Enabled) before any test runs.
- Tests that flip the live switch restore the configured value instead of
  leaving it off.
- Diagnostics tests compare against the captured value; the fresh-load
  startup test sets the switch explicitly for each load (adding the
  switch-off cases) and runs in the same collection as the switch mutators.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Root-owner rename walks RichTextBlock inline UI children

Reactor-managed (Route A) InlineUIContainer children live in a RichTextBlock's
document blocks, not its visual tree, until it is realized; the rename walk
now visits the same set UnmountInlineUIChildren tears down.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Report propagated root failures; time root throws before logging; docs

- Both hosts report a root Render() left by an exception the app asked to
  propagate (no fallback is shown, but the render ran), then rethrow.
- A root throw is timed at catch entry, before any debugger break, logging,
  or hook-order recovery work.
- ReactorTrace.GetComponentControl documents every case in which an id does
  not resolve (yet).
- CHANGELOG records the host root lifecycle fix under Fixed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Name the root on an app-supplied render-error fallback

When a root throws and the app's RenderErrorHandler supplies a fallback, both
hosts install it without root publication. The fallback's content root (its
internal guard) now gets the host-root fields (root=, the root's hooks=) added
to its own value, like a keyed-memo root; the fallback's controls keep their
own attribution.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Finish root retirement even when a cleanup throws

RetireRoot runs the outgoing root's cleanups through RenderErrorDispatch,
like Dispose (issue #1291): with a RenderErrorHandler every failure is
reported and the rest still run; with none the first escapes. Either way
the retirement completes (hook state cleared without re-running cleanups,
slots cleared, diagnostics reset) and Mount installs the new root before
rethrowing, so a failing cleanup can neither block the replacement nor stay
armed for the next Mount. The propagated-root fixture owns its nested window
and hosts through a disposable scope.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Catch only the expected exception type in the cleanup-failure fixture

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Drop stale static facts after hot reload or a late registration

- A hot-reload update makes every declared name and hook list unknown
  (the tables describe the compiled build; an edit can move a call onto a
  location the table gives another element).
- A source-mapped assembly registered after the table was built bumps a
  facts revision (it can make another assembly's file unattributable).
- On its next render each host walks its tree once per revision change and
  drops name=/hooks= fields that no longer hold, including on controls that
  were not re-published or were skipped.
- The selftest runner restores the compiled tables after each fixture, since
  hot-reload fixtures invalidate them process-wide.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Release the replaced root's tree when the new root renders nothing

After a root swap the previous root's reconciled tree stays on screen until
the replacement renders. If that first render returns null, nothing
reconciled it away, so its components (and their effects) stayed live. Both
hosts now release it in that case: the old tree is reconciled to null and no
content is shown. A root that merely re-renders to null keeps its content.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Flush a root's effects when it renders null

A root whose Render() returns null still commits: both hosts now flush the
root context's effects on that path too (failures routed as Effects), so a
root UseEffect runs and has a cleanup to retire later.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address review: exact sites for stale facts; hot reload invalidates regardless of switch

- The stale-facts refresh checks each control's exact call site (its tag where
  it has one), so when two files share one at= text only the stale one loses
  its name=/hooks=.
- HotReloadService invalidates the static facts without the devtools switch
  guard: SourceLocation.DeclaredName reads the same tables in any
  source-mapped build.
- Fix a Release-only nullable warning in the stale-facts fixture.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Performance guide: the provider has fifteen keywords, not seven

The regenerated keyword table lists fifteen (including RenderDetail); the
prose now says so, keeps the seven render-loop keywords' descriptions, and
points to Perf instrumentation and Diagnostics for the rest.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Shallow-skip tag refresh honours handler-owned teardown

An unstamped, callback-free item host that gains its call site through a
shallow skip (root, positional or mixed-key) now gets the tag its handler's
teardown is dispatched through in the Native AOT skip mode, as on mount and
update. RefreshCallSiteTagOnSkip is an instance method consulting the
handler (IV1TeardownOwner).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Release the replaced root's tree when the replacement fails

A replacement root that fails before committing took the built-in panel
path, which abandons the current tree unless it is an app fallback; the
previous root's descendants then stayed live off-screen. A pending root
replacement now counts as a tree to release in both hosts' ShowErrorFallback.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Finish releasing a replaced root's tree when a cleanup throws

ReleaseReplacedTree now tears the old tree down under
IsolateUnmountCleanupFailures, like a render-error release: every cleanup
runs, failures are logged, and a requested propagation is rethrown after the
release. The pending-replacement marker is cleared only once the release has
succeeded, so a failure still routes the outer render-error path to release
the tree instead of abandoning it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Root-owner and stale-facts walks reach TreeView node content

Legacy TreeViewNodeData.ContentElement controls are mounted for every node,
collapsed ones included, and sit in no visual or logical child walk; the
shared publish walk now visits TreeView.RootNodes recursively.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Route a replaced tree's cleanup failures like disposal's

ReleaseReplacedTree still tears the whole old tree down under
IsolateUnmountCleanupFailures, but now collects the failures and, once the
release is done, routes them through the new
RenderErrorDispatch.ReportCleanupFailures: each is reported to the
RenderErrorHandler as Cleanup; with no handler the first is rethrown (and
surfaces through the render loop's error path) instead of only being logged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Stamp the column on root mount sites

ReactorHostInfo.MountSite (#1318) was recorded as file + line only; with
SourceLocation.ColumnNumber available, the root-mount interceptors now pass
the column too.

- ReactorSourceMap.EnterRootMountSite(string filePath, int lineNumber, int
  columnNumber) replaces the 2-arg overload (unreleased infrastructure API,
  EditorBrowsable.Never; the generator in the same package is its only
  caller).
- The generator stamps the method name's column for ReactorApp.Run /
  OpenWindow, ReactorWindow.Mount and ReactorHost / ReactorHostControl.Mount,
  using the same CallSiteColumn rule as element call sites. RootMountSite
  model equality includes the column.
- Tests: generator driver (same-line Run/Mount calls, split name/paren),
  live RootMountInterceptionTests column oracle against the test file's text,
  and a runtime pass-through test. Source-mapping guide updated.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Use Lock for the host and render-trace gates; update host diagnostics fixture for render remounts

IDE0330 (enabled by #1351) rejects object lock gates in Release. The HostDiag fixture from #1318 asserted a render-function remount on a component root was ignored; this branch makes it replace the root.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Detach retired root contexts from their host; unmap a replacement adopted while tracing is off

A retired root's RenderContext kept its rerender callback and context scope, so a retained instance or hook setter pinned the host and re-rendered the replacement root. TryAdoptRealizedReplacement left the id on the discarded replacement wrapper when the listener was disabled mid-adoption.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Make SourceLocation.ColumnNumber a required positional member

Drops the extra constructor and init-only property; every call site now passes the column (0 when unknown).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Drain every root cleanup on retirement; route a swapped-out tree's cleanup failures to the handler

With no RenderErrorHandler, a throwing root cleanup stopped the rest, which retirement then dropped. Cleanup failures in a swapped-out root's tree released by a throwing replacement were only logged. Also time a child hook-order retry after the reset and before logging, add a ReactorHostControl root-swap fixture, and consolidate the duplicated ReactorSourceMap row in the packed skill.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* ReactorSource: root render-function hooks via the mount site; shared component names

Folds in the integration follow-up (2495eb4b7) now that #1318 and #1323 are
on main: a root render function's hooks= resolves from its recorded mount
site, and owner/mounts/root names forward to ComponentNames.For. Also adopts
System.Threading.Lock for the static-facts gate (IDE0330, now enforced in
Release).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Report a root that mounts its replacement mid-render as itself; recheck the gate after mount; resolve the released tree's handler per failure

The hosts capture the root and its diagnostics at render entry and replace (not reset) the diagnostics on retirement, so an attempt that called Mount keeps its name and id and the replacement's first render is its own mount. TrackMountedComponent rechecks ComponentRenderTrace.IsEnabled (a listener may dispose itself in the callback). ReportReleasedTreeCleanupFailures resolves the handler for each failure and logs one that has none.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Dedupe the build-properties table; skip the root hooks check where call sites are not stamped

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Defer re-entrant root retirement to the next pass; recheck the trace gate before mapping a re-rendered wrapper

Mount called from the outgoing root's own Render() or effect no longer clears and detaches contexts that code is still running against: the retirement is queued and runs at the start of the next pass (or on Dispose), so an effect's returned cleanup is not orphaned. EmitComponentRendered rechecks ComponentRenderTrace.IsEnabled before mapping, covering a listener disposed on a hook-order retry's aborted attempt.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Keep the teardown tag for single-content strategies in the AOT tag skip

A single-content strategy writes its child through an opaque setter, so the
generic unmount walk is not proven to reach it (a ContentControl that keeps its
child outside Content). Only a panel strategy whose collection is the control's
own Panel.Children is exempt now. Adds a live custom-slot regression in both
tag modes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Retire every deferred root; snapshot the rendering root after deferred retirement

RetireDeferredRoots skipped the rest of the queue after the first failure (??= short-circuits). The rendering-root snapshot is now taken after deferred retirement, so a root mounted by a deferred cleanup is reported as itself.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Time ReactorHost's root ComponentRendered from the root's Render() call

The PERF tree-phase stopwatch also covers the charting-state push, which is not part of the root's render.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Detach deferred roots on Dispose

Dispose cleaned up roots queued for deferred retirement but did not detach their contexts, so a retained old root or setter could pin the disposed host. Route them through RetireContexts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Publish the root a pass renders, not the live root, in both hosts

A child can Mount() a replacement root during reconciliation. The pass still
renders the outgoing root, so its root value, owner and hooks now come from a
snapshot taken with _renderingRoot (plus the mount site), and the replacement's
next pass renames the retained descendants. Adds a reentrant-remount regression
for ReactorHost and ReactorHostControl.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Keep the pending tree release when a root is replaced mid-pass

A root that mounts its replacement from its own Render() still commits its tree (or error panel) in that pass. That content now stays marked for release, so a replacement whose first render is null removes it instead of leaving it and its effects live.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Record the mount site when the active root instance is mounted again

Re-mounting the current root keeps it and its effects, but the diagnostics site now follows the latest explicit call (as on main).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Name a null-returning root's effect failure after the root

The null-tree path flushed the class root's effects without capturing effectsRoot, so a failure was reported as FuncElement with no component name.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Include root mount sites in the static-info file fingerprints

A file whose only Reactor call is a hook-free root mount (output built with
constructors) made no b.Source claim, so another assembly's hook-using root
at the same path, line and column lent it its hooks. Every root mount site now
records its (possibly empty) render-function hooks fact. Generator regression
plus docs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Report every render of a hot-reload pass as hotReload

ReactorHostControl's hot-reload passes are not forced, so a child reached from its parent reported props/context/parent. ClassifyUpdate now gives an open hot-reload pass precedence, and the hook-order retry marker is independent of the force flag (both hosts set it) and dropped when its pass ends.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Regenerate reconciler snippets; scope hotReload to re-renders

The architecture-overview and reconciliation guides embed the reconciler-entry snippet, which still named the old retry fields. A component first created during a hot-reload pass is a new instance and reports mount; the docs now say so.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Drop stale static facts from content a null-rendering root keeps

Both hosts returned before any publication when the root rendered null, so
content it kept held obsolete name=/hooks= after a hot-reload update or a late
conflicting registration. The null-tree branch now runs the reconciler's
stale-fact refresh on the retained content, under the diagnostics gates.
Non-null to null regression for ReactorHost and ReactorHostControl.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Keep a root mounted by the outgoing root's cleanup

RetireRoot clears the root slots before running the outgoing cleanups, and Mount bumps a generation: when a cleanup mounted another root, the outer Mount leaves it in place instead of retiring and overwriting it, matching the deferred path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Re-describe a null-rendering root's retained content, host hooks included

A function root's hooks= is added by the host from its mount site, so the
stale-fact walk alone could not drop it after a late conflicting registration.
A null render now re-publishes the retained root as a publishing pass would
(PublishRootSource, PublishFallbackRootSource for an app fallback, or the
stale-fact walk for the built-in error panel). Late-conflict null-render
regression for both hosts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Resolve target-wrapping decorators before the publish walk's child enumeration

A Flyout replaces its target's tag with FlyoutElement, whose adapter enumerates
no children, so a decorated named-slot host (Flyout(NavigationView(...), ...))
left an unrealized PaneHeader outside root-owner renames and stale-fact
invalidation. The walk now unwraps decorators (ReactorSourceMap.UnwrapDecorators,
cycle-safe) first. Live regression in ReactorSource_RootRemountRenamesOwner.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Enumerate panel-strategy collections in the publish walk's live-child visit

A panel strategy's collection can belong to an inner panel the host's template
has not attached, outside the visual and logical walks, so root renames and
stale-fact invalidation missed its children. VisitLiveChildren now visits it.
Live regression with a custom untemplated host.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Document that a host root's componentId only maps forward

Cherry-picked from 722b41ec6, #1328's post-merge doc fix (pushed after the
squash merge, so not on main).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Address github-code-quality notes on ReactorSource

Combine the nested hot-reload check in WithoutStaleFacts, filter the three
diagnostics-only loops (navigation cache snapshot, inline UI paragraphs,
longest-prefix root) up front, and make the keyed-memo fixture's factory
counter a local.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Filter up front in the source-map generator's static-info loops

Follow-up code-quality notes: Where/Select instead of skip-and-continue in the
generator's fact, hook-slot and destructuring loops, and MaxBy for the longest
root prefix. No behaviour change (generator snapshot and root-relative tests).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Describe a decorated control by its target's call site only

A stamped decorator over an unstamped target published the decorator's
location as the target control's at= (and name=), while GetSource reports no
location for it. Format and IdentityChanged now take the call site from the
unwrapped target alone, exactly as GetSource does. Unit test plus a live
mount/update fixture. Also: code-quality notes (Select in the generator's
hook-slot walk, a documented best-effort catch, Path.Join in two tests).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Gate ReactorSource stale-fact fixtures on an independent stamping probe

The null-render, late-conflict and stale-facts fixtures decided whether the
host stamps call sites from the published value under test, so a host that
stopped publishing at= skipped (and the HostControl path ignored the result)
instead of failing. They now probe a factory call's CallSite with source
mapping on, and once stamping is established require at= with H.Check on every
host path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Let a source-mapped plug-in in a collectible load context unload

ReactorSource diagnostics kept a collectible AssemblyLoadContext alive when the
plug-in shares the host's Reactor:
- the pending static-info registrations (assembly + fill callback) and the
  built table's assembly set and component-hook keys held the assembly
  strongly. Both are now owned weakly by the assembly (ConditionalWeakTable);
  location-keyed tables hold strings only;
- the published-value cache keyed shapes by element Type; it now keys by the
  formatted kind string, cached per type in a ConditionalWeakTable;
- ControlRegistry's source-target and base-dispatch caches, which every
  published control goes through, no longer cache collectible types.

Unload regression tests (before and after the first table lookup, and after
formatting a plug-in element), each mutation-checked.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Clear a closed ContentDialog's published ReactorSource

The dialog's close cleanup cleared only the element pointer, so a dialog an app
keeps through .Set(...) still carried its ReactorSource, which Native AOT's
GetSource falls back to (JIT already returned null). It now uses
ClearElementTag, which also clears the published value. Live check in both tag
modes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] RenderError names ComponentElement`1 instead of the component, and misses mount, host-root and ErrorBoundary-caught errors

2 participants